// MIT License
//
// Copyright (c) 2017-2019 MessageKit
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import AVFoundation
import MessageKit
import UIKit

// MARK: - PlayerState

/// The `PlayerState` indicates the current audio controller state
public enum PlayerState {
  /// The audio controller is currently playing a sound
  case playing

  /// The audio controller is currently in pause state
  case pause

  /// The audio controller is not playing any sound and audioPlayer is nil
  case stopped
}

// MARK: - BasicAudioController

/// The `BasicAudioController` update UI for current audio cell that is playing a sound
/// and also creates and manage an `AVAudioPlayer` states, play, pause and stop.
@MainActor
open class BasicAudioController: NSObject, @preconcurrency AVAudioPlayerDelegate {
  // MARK: Lifecycle

  // MARK: - Init Methods

  public init(messageCollectionView: MessagesCollectionView) {
    self.messageCollectionView = messageCollectionView
    super.init()
  }

  // MARK: Open

  /// The `AVAudioPlayer` that is playing the sound
  open var audioPlayer: AVAudioPlayer?

  /// The `AudioMessageCell` that is currently playing sound
  open weak var playingCell: AudioMessageCell?

  /// The `MessageType` that is currently playing sound
  open var playingMessage: MessageType?

  /// Specify if current audio controller state: playing, in pause or none
  open private(set) var state: PlayerState = .stopped

  // MARK: - Methods

  /// Used to configure the audio cell UI:
  ///     1. play button selected state;
  ///     2. progressView progress;
  ///     3. durationLabel text;
  ///
  /// - Parameters:
  ///   - cell: The `AudioMessageCell` that needs to be configure.
  ///   - message: The `MessageType` that configures the cell.
  ///
  /// - Note:
  ///   This protocol method is called by MessageKit every time an audio cell needs to be configure
  open func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {
    if playingMessage?.messageId == message.messageId, let collectionView = messageCollectionView, let player = audioPlayer {
      playingCell = cell
      cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime / player.duration)
      cell.playButton.isSelected = (player.isPlaying == true) ? true : false
      guard let displayDelegate = collectionView.messagesDisplayDelegate else {
        fatalError("MessagesDisplayDelegate has not been set.")
      }
      cell.durationLabel.text = displayDelegate.audioProgressTextFormat(
        Float(player.currentTime),
        for: cell,
        in: collectionView)
    }
  }

  /// Used to start play audio sound
  ///
  /// - Parameters:
  ///   - message: The `MessageType` that contain the audio item to be played.
  ///   - audioCell: The `AudioMessageCell` that needs to be updated while audio is playing.
  open func playSound(for message: MessageType, in audioCell: AudioMessageCell) {
    switch message.kind {
    case .audio(let item):
      playingCell = audioCell
      playingMessage = message
      guard let player = try? AVAudioPlayer(contentsOf: item.url) else {
        print("Failed to create audio player for URL: \(item.url)")
        return
      }
      audioPlayer = player
      audioPlayer?.prepareToPlay()
      audioPlayer?.delegate = self
      audioPlayer?.play()
      state = .playing
      audioCell.playButton.isSelected = true // show pause button on audio cell
      startProgressTimer()
      audioCell.delegate?.didStartAudio(in: audioCell)
    default:
      print("BasicAudioPlayer failed play sound because given message kind is not Audio")
    }
  }

  /// Used to pause the audio sound
  ///
  /// - Parameters:
  ///   - message: The `MessageType` that contain the audio item to be pause.
  ///   - audioCell: The `AudioMessageCell` that needs to be updated by the pause action.
  open func pauseSound(for _: MessageType, in audioCell: AudioMessageCell) {
    audioPlayer?.pause()
    state = .pause
    audioCell.playButton.isSelected = false // show play button on audio cell
    progressTimer?.invalidate()
    if let cell = playingCell {
      cell.delegate?.didPauseAudio(in: cell)
    }
  }

  /// Stops any ongoing audio playing if exists
  open func stopAnyOngoingPlaying() {
    guard
      let player = audioPlayer,
      let collectionView = messageCollectionView
    else { return } // If the audio player is nil then we don't need to go through the stopping logic
    player.stop()
    state = .stopped
    if let cell = playingCell {
      cell.progressView.progress = 0.0
      cell.playButton.isSelected = false
      guard let displayDelegate = collectionView.messagesDisplayDelegate else {
        fatalError("MessagesDisplayDelegate has not been set.")
      }
      cell.durationLabel.text = displayDelegate.audioProgressTextFormat(
        Float(player.duration),
        for: cell,
        in: collectionView)
      cell.delegate?.didStopAudio(in: cell)
    }
    progressTimer?.invalidate()
    progressTimer = nil
    audioPlayer = nil
    playingMessage = nil
    playingCell = nil
  }

  /// Resume a currently pause audio sound
  open func resumeSound() {
    guard let player = audioPlayer, let cell = playingCell else {
      stopAnyOngoingPlaying()
      return
    }
    player.prepareToPlay()
    player.play()
    state = .playing
    startProgressTimer()
    cell.playButton.isSelected = true // show pause button on audio cell
    cell.delegate?.didStartAudio(in: cell)
  }

  // MARK: - AVAudioPlayerDelegate
  open func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully _: Bool) {
    stopAnyOngoingPlaying()
  }

  open func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error _: Error?) {
    stopAnyOngoingPlaying()
  }

  // MARK: Public

  // The `MessagesCollectionView` where the playing cell exist
  public weak var messageCollectionView: MessagesCollectionView?

  // MARK: Internal

  /// The `Timer` that update playing progress
  internal var progressTimer: Timer?

  // MARK: Private

  // MARK: - Fire Methods
  @objc
  private func didFireProgressTimer(_: Timer) {
    guard let player = audioPlayer, let collectionView = messageCollectionView, let cell = playingCell else {
      return
    }
    // check if can update playing cell
    if let playingCellIndexPath = collectionView.indexPath(for: cell) {
      // 1. get the current message that decorates the playing cell
      // 2. check if current message is the same with playing message, if so then update the cell content
      // Note: Those messages differ in the case of cell reuse
      let currentMessage = collectionView.messagesDataSource?.messageForItem(at: playingCellIndexPath, in: collectionView)
      if currentMessage != nil, currentMessage?.messageId == playingMessage?.messageId {
        // messages are the same update cell content
        cell.progressView.progress = (player.duration == 0) ? 0 : Float(player.currentTime / player.duration)
        guard let displayDelegate = collectionView.messagesDisplayDelegate else {
          fatalError("MessagesDisplayDelegate has not been set.")
        }
        cell.durationLabel.text = displayDelegate.audioProgressTextFormat(
          Float(player.currentTime),
          for: cell,
          in: collectionView)
      } else {
        // if the current message is not the same with playing message stop playing sound
        stopAnyOngoingPlaying()
      }
    }
  }

  // MARK: - Private Methods
  private func startProgressTimer() {
    progressTimer?.invalidate()
    progressTimer = nil
    progressTimer = Timer.scheduledTimer(
      timeInterval: 0.1,
      target: self,
      selector: #selector(BasicAudioController.didFireProgressTimer(_:)),
      userInfo: nil,
      repeats: true)
  }
}
